<?php
namespace diszz\phpyac;

use Yii;
use yii\base\Component;

/**
 * 基于yii2的yac扩展类
 * 适用于本机缓存!
 * cli模式进程间无法共享!
 * 
 * 普通模式配置方法:
 * 'components' => [
        'yac' => [
            'class' => 'diszz\phpyac\Connection',
            'keyPrefix' => 'yac:',
        ],
    ]
 *   
 *   //获取值
 *   $a = Yii::$app->yac->get('ddd');
 *   
 *   //设置值
 *   $a = Yii::$app->yac->set('ddd', time(), 60);
 *   
 *   //添加缓存, 若存在则失败
 *   $a = Yii::$app->yac->add('ddd', time(), 60);
 *   
 *   //删除值
 *   $a = Yii::$app->yac->delete('ddd');
 *
 * Class Connection
 * @package diszz\phpredis
 */
class Connection extends Component
{
    /**
     * @var string
     */
    public $keyPrefix = '';
    
    /**
     * Redis connection
     */
    protected $yac;
    
    /**
     * Initializes the redis Session component.
     * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
     */
    public function init()
    {
        $this->open();
        
        parent::init();
    }
    
    /**
     * Returns the fully qualified name of this class.
     * @return string the fully qualified name of this class.
     */
    public static function className()
    {
        return get_called_class();
    }

    /**
     * Establishes a DB connection.
     * It does nothing if a DB connection has already been established.
     */
    public function open()
    {
        if ($this->yac !== null) {
            \Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' had open ', __CLASS__);
            return;
        }
        
        $this->yac = new \Yac();
        
        //\Yii::info(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' succ ', __CLASS__);
        //------------------------------------------
    }
    
    //这个方法可以将以上sentinel返回的信息解析为数组
    public function parseArrayResult(array $data)
    {
        $result = array();
        $count = count($data);
        for ($i = 0; $i < $count;) {
            $record = $data[$i];
            if (is_array($record)) {
                $result[] = $this->parseArrayResult($record);
                $i++;
            } else {
                $result[$record] = $data[$i + 1];
                $i += 2;
            }
        }
        return $result;
    }
    
    public function buildKey($key)
    {
        if (is_string($key)) {
        } else {
            $key = json_encode($key);
        }
        
        //yac仅支持48个字符!
        if (strlen($key) + strlen($this->keyPrefix) > 48) {
            $key_ = md5($key);
            //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key.';'.$key_, __CLASS__);
        }else{
            $key_ = $key;
        }
        
        return $this->keyPrefix . $key_;
    }

    public function exists($key)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->yac->get($this->buildKey($key));
    }
    
    public function get($key)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->yac->get($this->buildKey($key));
    }
    
    public function mget($keys)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.json_encode($keys), __CLASS__);
        
        foreach ($keys as $k => $v) {
            $keys[$k] = $this->buildKey($v);
        }
        return $this->yac->get($keys);
    }
    
    public function set($key, $val, $ttl)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->yac->set($this->buildKey($key), $val, $ttl);
    }
    
    /**
     * Yac::set($key, $value[, $ttl = 0])
     * Yac::set(array $kvs[, $ttl = 0])
     * 
     * $yac->set("foo", "bar");
     * $yac->set(array("dummy" => "foo","dummy2" => "foo"));
     * 
     * @param array $vals
     * @param int $expire
     */
    public function mset($kvs, $expire)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.json_encode($kvs), __CLASS__);
        
        $kvs_ = [];
        foreach ($kvs as $kk => $kv) {
            $kvs_[$this->buildKey($kk)] = $kv;
        }
        return $this->yac->set($kvs_, $expire);
    }
    
    /**
     * 
     * bool, true on success, false on failure
     * 
     * @param string $key
     * @param string $val
     * @param int $ttl
     */
    public function add($key, $val, $ttl)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->yac->add($this->buildKey($key), $val, $ttl);
    }
    
    public function del($key)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->yac->delete($this->buildKey($key));
    }
    
    public function delete($key)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->del($key);
    }
    
    public function incr($key, $step = 1, $ttl = 30)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        $value = $this->get($key);
        $value = ($value ? $value + $step : 1);
        $this->set($key, $value, $ttl);
        return $value;
    }
    
    public function incrby($key, $step = 1, $ttl = 30)
    {
        return $this->incr($key, $step, $ttl);
    }
    
    public function increase($key, $step = 1, $ttl = 30)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->incr($key, $step, $ttl);
    }
    
    public function decr($key = '', $step = 1, $ttl = 30)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        $value = $this->get($key);
        $value = ($value >= $step ? $value - $step : 0);
        $this->set($key, $value, $ttl);
        return $value;
    }
    
    public function decrby($key, $step = 1, $ttl = 30)
    {
        return $this->decr($key, $step, $ttl);
    }
    
    public function decrease($key, $step = 1, $ttl = 30)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        return $this->decr($key, $step, $ttl);
    }
    
    /**
     * 
     * true on success, false on failure
     * 
     * @param string $key
     * @param number $ttl
     * @return boolean
     */
    public function lock($key, $ttl = 1)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        
        $cacheKey = 'slock:'.$key;
        
        if ($this->get($cacheKey)) {
            return false;
        }
        
        //bool, true on success, false on failure
        if ($this->add($cacheKey, 1, $ttl) && $this->get($cacheKey)) {
            return true;
        } else {
            return false;
        }
    }
    
    public function unLock($key)
    {
        return $this->delete('slock:'.$key);
    }
    
    public function info()
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test ', __CLASS__);
        
        return $this->yac->info();
    }
    
    public function flushdb()
    {
        \Yii::error(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test ', __CLASS__);
        
        return $this->yac->flushDB();
    }
    
    /**
     * 
     * demo:
     * 
     * public static function getMatchTwoJoinCount() {
        return Yii::$app->redis->getOrSet('matchTwoJoinCount', function() use ($type){
            
            $where = ['status' => DatasetMatchUser::STATUS_OVER, 'type' => $type];
            list($matchTwoJoinCount) = DatasetMatchUser::getCount($where);
            return $matchTwoJoinCount;
            
        }, 1800);
    }
     * 
     * @param string $key
     * @param $callable
     * @param number $expire
     * @param number $lockedTimes
     * @return boolean|boolean|mixed|mixed
     */
    public function getOrSet($key, $callable, $expire = 1800, $resetCache = false, $lockedTimes = 0)
    {
        $_logs = ['$key' => $key, '$expire' => $expire, '$resetCache' => $resetCache, '$lockedTimes' => $lockedTimes];
        $_logs['starttime'] = microtime(true);
        
        //$cacheKey = 'getOrSet:'.$key;
        $cacheKey = $key;
        //$_logs['$cacheKey'] = $cacheKey;
        
        $cacheData = $this->get($cacheKey);
        //$_logs[__LINE__] = microtime(true);
        
        //没有找到缓存, 则去查询
        if($cacheData === false || $resetCache){
            
            $lockCacheKey = $cacheKey.':setnx';
            //$_logs['$lockCacheKey'] = $lockCacheKey;
            
            //并发锁, 防止缓存击穿. 返回1:未锁; 返回0:已锁
            if ($this->lock($lockCacheKey, 1, 2)){
                //Yii::info(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' lock succ '. json_encode($_logs));
            } else {
                
                //避免死循环, 被锁3次, 立即返回
                if ($lockedTimes > 3) {
                    Yii::error(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' locked, locktimes > 3 '.json_encode($_logs));
                    return false;
                }
                
                //延迟0.3秒
                usleep(300000);
                
                //第一次并发不记录
                if ($lockedTimes > 0) {
                    Yii::error(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' usleep '. json_encode($_logs));
                }
                
                return $this->getOrSet($key, $callable, $expire, $resetCache, $lockedTimes+1);
            }
            
            //----------------------
            
            //$_logs[__LINE__] = microtime(true);
            
            $cacheData = call_user_func($callable, $this);
            
            //$_logs[__LINE__] = microtime(true);
            
            $this->set($cacheKey, serialize($cacheData), $expire);
            
            //$_logs[__LINE__] = microtime(true);
            
            $this->unLock($lockCacheKey);
            
            $_logs['endtime'] = microtime(true);
            if ($_logs['endtime'] - $_logs['starttime'] > 0.1) {
                Yii::error(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' timeout110 '. json_encode($_logs));
            }
        } else {
            $cacheData = unserialize($cacheData);
            
            $_logs['endtime'] = microtime(true);
            if ($_logs['endtime'] - $_logs['starttime'] > 0.1) {
                Yii::error(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' timeout110 '. json_encode($_logs));
            }
        }
        
        //$_logs['$cacheData'] = $cacheData;
        //Yii::info(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' succ '. json_encode($_logs));
        return $cacheData;
    }
    
    /**
     * hotkey
     * 自身数量+1,返回总数量
     * 
     * @param string $key
     * @param number $expire
     * @param number $times
     * @return boolean
     */
    public function hotkey($key, $expire = 600, $times = 30){
        //$_logs = ['$key' => $key, '$expire' => $expire, '$times' => $times];
        //$_logs['starttime'] = microtime(true);
        
        $hotCount = $this->incr('hotkey:'.date('ymdH').$key, 1, $expire);
        //$_logs['$hotCount'] = $hotCount;
        
        if ($hotCount > $times) {
            //Yii::info(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' yes '. json_encode($_logs));
            return true;
        }
        
        //Yii::info(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' no '. json_encode($_logs));
        return false;
    }

    /**
     * yac多服务器之间的数据一致性
     * 同步删除服务器群组中的各个服务器的yac
     * 
     * @var string
     */

    public $redis = 'redis';
    public $server_id = '0';

    public function redisInit(){
        if (is_string($this->redis)) {
            $this->redis = Yii::$app->get($this->redis);
            $this->redis->open();
        }
    }

    /**
     * 添加到同步队列
     *
     * @param string $key
     * @return bool
     */
    public function addtoYacSyncDeleteList($key)
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        $this->redisInit();

        $cacheKey = 'yac_sync_delete_list'.date('ymd');
        $this->redis->zadd(cacheKey, time(), $key);

        return $this->del($key);
    }

    /**
     * 清除当前服务器的同步队列
     */
    public function cleanYacSyncDeleteList()
    {
        //\Yii::trace(__CLASS__.' '.__FUNCTION__.' '.__LINE__.' test '.$key, __CLASS__);
        $this->redisInit();

        $cacheKey = 'yac_sync_delete_list'.date('ymd');
        $arr = $this->redis->zrevrangebyscore($cacheKey, time(), time() - 600);

        if (empty($arr)){
            return ;
        }

        $cleanCacheKey = 'yac_sync_delete_list'.date('ymd').$this->server_id;
        $cleanArr = $this->redis->zrevrangebyscore($cleanCacheKey, time(), time() - 600);

        if ($cleanArr){
            $newArr = array_diff($arr, $cleanArr);
        }else{
            $newArr = $arr;
        }

        if ($newArr){
            foreach($newArr as $key){
                $this->del($key);
                $this->redis->zadd($cleanCacheKey, time(), $key);
            }
        }

    }
    
}
